// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    internal class UpdateBatchExecutor : UpdateExecutor
    {
        public static UpdateBatchExecutor Instance => new UpdateBatchExecutor();

        public virtual UpdateResult Update(
            DataContext dataContext,
            IReadOnlyList<IUpdatable> updatables)
        {
            var result = new UpdateResult();
            var saveContext = new SaveContext();

            var updateableBatch = new List<IUpdatable>();

            foreach (var updatable in updatables)
            {
                if ((updatable.UpdateType != UpdateType.ModelUpdate ||
                    !((IUpdatableModel)updatable).IgnoreSave) &&
                        updatable.UpdateType != UpdateType.Action &&
                        updatable.UpdateType != UpdateType.OtherUpdatable)
                {
                    updateableBatch.Add(updatable);
                }

                if (updatable.UpdateType == UpdateType.ModelUpdate)
                {
                    var updatableModel = (IUpdatableModel)updatable;

                    if (updatableModel.IgnoreSave &&
                        updatableModel.ModelMeta is IPocoModelMeta modelMeta &&
                        modelMeta.HasSetValueProperties)
                    {
                        if (updateableBatch.Count > 0)
                        {
                            result = this.UpdateBatch(dataContext, result, updateableBatch);
                            updateableBatch.Clear();
                        }

                        updatableModel.PerformSetValueOperations();

                        continue;
                    }

                    if (updatableModel is ISaveAction action1 && action1.HasBeforeAction)
                    {
                        if (updateableBatch.Count > 1)
                        {
                            var tempBatch = updateableBatch.Take(updateableBatch.Count - 1)
                                                            .ToList();
                            result = this.UpdateBatch(dataContext, result, tempBatch);
                            updateableBatch.Clear();

                            //execute batch exclude the current updatableModel if it's not the first in the batch
                            //Add the current updatableModel back to the batch
                            updateableBatch.Add(updatableModel);
                        }

                        //Check if set Cancelled flag in action
                        if (this.ExecuteSaveAction(updatableModel, SaveActionBehavior.BeforeSave, saveContext))
                        {
                            result.Cancelled = true;
                            break;
                        }
                    }

                    if (updatableModel is ISaveAction action2 && action2.HasAfterAction)
                    {
                        result = this.UpdateBatch(dataContext, result, updateableBatch);
                        updateableBatch.Clear();

                        //Check if set Cancelled flag in action
                        if (this.ExecuteSaveAction(updatableModel, SaveActionBehavior.AfterSave, saveContext))
                        {
                            result.Cancelled = true;
                            break;
                        }
                    }

                    if (updatableModel.ModelMeta is IPocoModelMeta modelMeta1 &&
                        modelMeta1.HasSetValueProperties)
                    {
                        if (updateableBatch.Count > 0)
                        {
                            result = this.UpdateBatch(dataContext, result, updateableBatch);
                            updateableBatch.Clear();
                        }

                        updatableModel.PerformSetValueOperations();
                    }
                }
                else if (updatable.UpdateType == UpdateType.Action)
                {
                    result = this.UpdateBatch(dataContext, result, updateableBatch);
                    updateableBatch.Clear();

                    //Check if set Cancelled flag in action
                    if (this.ExecuteSaveAction((IUpdatableAction)updatable, saveContext))
                    {
                        result.Cancelled = true;
                        break;
                    }
                }
                else if (updatable.UpdateType == UpdateType.OtherUpdatable)
                {
                    result = this.UpdateBatch(dataContext, result, updateableBatch);
                    updateableBatch.Clear();

                    this.ExecuteOtherUpdatable(dataContext, (UpdatableObject)updatable, ref result);
                }
            }

            if (!saveContext.Cancelled)
            {
                result = this.UpdateBatch(dataContext, result, updateableBatch);
            }

            return result;
        }

        public virtual async Task<UpdateResult> UpdateAsync(
            DataContext dataContext,
            IReadOnlyList<IUpdatable> updatables,
            CancellationToken cancellationToken)
        {
            var result = new UpdateResult();
            var saveContext = new SaveContext();

            var updateableBatch = new List<IUpdatable>();

            foreach (var updatable in updatables)
            {
                if ((updatable.UpdateType != UpdateType.ModelUpdate ||
                    !((IUpdatableModel)updatable).IgnoreSave) &&
                        updatable.UpdateType != UpdateType.Action &&
                        updatable.UpdateType != UpdateType.OtherUpdatable)
                {
                    updateableBatch.Add(updatable);
                }

                if (updatable.UpdateType == UpdateType.ModelUpdate)
                {
                    var updatableModel = (IUpdatableModel)updatable;

                    if (updatableModel.IgnoreSave &&
                        updatableModel.ModelMeta is IPocoModelMeta modelMeta &&
                        modelMeta.HasSetValueProperties)
                    {
                        if (updateableBatch.Count > 0)
                        {
                            result = await this.UpdateBatchAsync(
                                dataContext, result, updateableBatch, cancellationToken);

                            updateableBatch.Clear();
                        }

                        updatableModel.PerformSetValueOperations();

                        continue;
                    }

                    if (updatableModel is ISaveAction action1 && action1.HasBeforeAction)
                    {
                        if (updateableBatch.Count > 1)
                        {
                            var tempBatch = updateableBatch.Take(updateableBatch.Count - 1)
                                                            .ToList();

                            result = await this.UpdateBatchAsync(dataContext, result, tempBatch, cancellationToken);
                            
                            updateableBatch.Clear();

                            //execute batch exclude the current updatableModel if it's not the first in the batch
                            //Add the current updatableModel back to the batch
                            updateableBatch.Add(updatableModel);
                        }

                        //Check if set Cancelled flag in action
                        if (this.ExecuteSaveAction(updatableModel, SaveActionBehavior.BeforeSave, saveContext))
                        {
                            result.Cancelled = true;
                            break;
                        }
                    }

                    if (updatableModel is ISaveAction action2 && action2.HasAfterAction)
                    {
                        result = await this.UpdateBatchAsync(
                            dataContext, result, updateableBatch, cancellationToken);
                       
                        updateableBatch.Clear();

                        //Check if set Cancelled flag in action
                        if (this.ExecuteSaveAction(updatableModel, SaveActionBehavior.AfterSave, saveContext))
                        {
                            result.Cancelled = true;
                            break;
                        }
                    }

                    if (updatableModel.ModelMeta is IPocoModelMeta modelMeta1 &&
                        modelMeta1.HasSetValueProperties)
                    {
                        if (updateableBatch.Count > 0)
                        {
                            result = await this.UpdateBatchAsync(
                                dataContext, result, updateableBatch, cancellationToken);

                            updateableBatch.Clear();
                        }

                        updatableModel.PerformSetValueOperations();
                    }
                }
                else if (updatable.UpdateType == UpdateType.Action)
                {
                    result = await this.UpdateBatchAsync(
                        dataContext, result, updateableBatch, cancellationToken);

                    updateableBatch.Clear();

                    //Check if set Cancelled flag in action
                    if (this.ExecuteSaveAction((IUpdatableAction)updatable, saveContext))
                    {
                        result.Cancelled = true;
                        break;
                    }
                }
                else if (updatable.UpdateType == UpdateType.OtherUpdatable)
                {
                    result = await this.UpdateBatchAsync(
                        dataContext, result, updateableBatch, cancellationToken);

                    updateableBatch.Clear();

                    this.ExecuteOtherUpdatable(dataContext, (UpdatableObject)updatable, ref result);
                }
            }

            if (!saveContext.Cancelled)
            {
                result = await this.UpdateBatchAsync(dataContext, result, updateableBatch, cancellationToken);
            }

            return result;
        }

        private UpdateResult UpdateBatch(
            DataContext dataContext,
            UpdateResult result,
            IReadOnlyList<IUpdatable> updatables)
        {
            if (updatables.Count == 0)
            {
                return result;
            }

            var batchUpdateCommand = new BatchUpdateCommand(dataContext);

            batchUpdateCommand.Execute(updatables);
            this.UpdateResult(ref result, batchUpdateCommand);

            return result;
        }

        private async Task<UpdateResult> UpdateBatchAsync(
            DataContext dataContext,
            UpdateResult result,
            IReadOnlyList<IUpdatable> updatables,
            CancellationToken cancellationToken)
        {
            if (updatables.Count == 0)
            {
                return result;
            }

            var batchUpdateCommand = new BatchUpdateCommand(dataContext);

            await batchUpdateCommand.ExecuteAsync(updatables, cancellationToken);

            this.UpdateResult(ref result, batchUpdateCommand);

            return result;
        }
    }
}
